package com.aizuda.encrypt.handler;

import cn.hutool.cache.Cache;
import cn.hutool.cache.CacheUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.aizuda.encrypt.entity.EncryptInfo;
import com.aizuda.encrypt.entity.EncryptRule;
import com.aizuda.encrypt.jsqlparse.entity.JSQLCryptExpression;
import com.aizuda.encrypt.jsqlparse.entity.JSQLTable;
import com.aizuda.encrypt.jsqlparse.helper.TablesNamesFinder;
import com.aizuda.encrypt.jsqlparse.visitor.ExpressionV;
import com.aizuda.encrypt.jsqlparse.visitor.FromItemV;
import com.aizuda.encrypt.util.ParseSQLUtil;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.select.FromItem;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.select.SelectBody;
import net.sf.jsqlparser.statement.update.Update;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * 语句上下文
 *
 * @date 2022-03-02 10:27:02
 */
@Slf4j
public class JSQLStatementContext {

    private static final Cache<String, List<JSQLTable>> lruCache = CacheUtil.newLRUCache(50);
    private final        EncryptRule                    encryptRule;
    private              Statement                      statements;
    private              List<JSQLTable>                tableList;
    private              List<JSQLCryptExpression>      expressionList;

    public JSQLStatementContext(String sql, EncryptRule encryptRule) throws JSQLParserException {
        final Statement statements = CCJSqlParserUtil.parse(sql);
        this.encryptRule = encryptRule;
        this.statements  = statements;
        // 解析表名，使用 LRU (least recently used)最近最久未使用缓存。
        List<JSQLTable> tableList = lruCache.get(sql);
        if (CollUtil.isEmpty(tableList)) {
            final TablesNamesFinder tablesNamesFinder = new TablesNamesFinder();
            tableList = tablesNamesFinder.getTableList(statements);
            lruCache.put(sql, tableList);
        }
        this.tableList = tableList;
    }

    /**
     * 获取解析后的sql表达式列表
     *
     * @return {@link List }<{@link JSQLCryptExpression }>
     * @author nn200433
     */
    public List<JSQLCryptExpression> getExpressionList() {
        return expressionList;
    }

    /**
     * 创建 EncryptInfo 列表对象
     *
     * @return {@link List<  EncryptInfo  > }
     * @author nn200433
     */
    public List<EncryptInfo> createEncryptInfos() {
        if (statements instanceof Select) {
            expressionList = parseSelectWhere((Select) statements);
        } else if (statements instanceof Insert) {
            expressionList = parseInsertColumns((Insert) statements);
        } else if (statements instanceof Update) {
            expressionList = parseUpdateColumns((Update) statements);
        }
        if (CollectionUtil.isEmpty(expressionList)) {
            return Collections.emptyList();
        }
        List<EncryptInfo> result = new LinkedList<EncryptInfo>();
        for (JSQLCryptExpression expression : expressionList) {
            result.addAll(createEncryptInfos(expression));
        }
        return result;
    }

    /**
     * 获取表名
     *
     * @param expression expression
     * @return {@link Optional<String> }
     * @author nn200433
     */
    public Optional<String> findTableName(JSQLCryptExpression expression) {
        final String columnName = expression.getColumnName();
        final String alias      = expression.getAlias();
        final int    tableSize  = tableList.size();
        if (tableSize == 1) {
            final JSQLTable first = tableList.get(0);
            return Optional.of(first.getTableName());
        } else if (StrUtil.isNotEmpty(alias)) {
            return Optional.ofNullable(findTableNameFromSQL(alias));
        }
        return findTableNameFromMetaData(columnName);
    }

    /**
     * 解析 select 的查询条件
     *
     * @param select 查询
     * @return {@link List<JSQLCryptExpression> }
     */
    private List<JSQLCryptExpression> parseSelectWhere(Select select) {
        log.debug("---> [Mybatis SQL 拦截器] -------------------- 解析表达式 开始 --------------------");
        log.debug("---> [Mybatis SQL 拦截器] 待解析的sql = {}", select.toString());
        List<JSQLCryptExpression> list = new ArrayList<JSQLCryptExpression>();
        try {
            final SelectBody        selectBody      = select.getSelectBody();
            final List<PlainSelect> plainSelectList = ParseSQLUtil.getPlainSelectList(selectBody);
            if (CollUtil.isEmpty(plainSelectList)) {
                return list;
            }
            for (PlainSelect plain : plainSelectList) {
                FromItem fromItem = plain.getFromItem();
                if (null != fromItem) {
                    fromItem.accept(new FromItemV(list));
                }
                Expression where = plain.getWhere();
                if (null != where) {
                    where.accept(new ExpressionV(list));
                }
            }
        } catch (Exception e) {
            log.error("---> [Mybatis 查询拦截器] 解析where条件出错", e.getMessage());
        }
        log.debug("---> [Mybatis SQL 拦截器] -------------------- 解析表达式 结束 --------------------");
        return list;
    }

    /**
     * 解析 insert 字段的表名
     *
     * @param insert 插入对象
     * @return {@link List<JSQLCryptExpression> }
     */
    private List<JSQLCryptExpression> parseInsertColumns(Insert insert) {
        final String              defaultTableName = insert.getTable().getName();
        final List<Column>        columns          = insert.getColumns();
        List<JSQLCryptExpression> result           = new ArrayList<>();
        for (int i = 0; i < columns.size(); i++) {
            JSQLCryptExpression expression = new JSQLCryptExpression();
            final Column        column     = columns.get(i);
            final String        columnName = column.getColumnName();
            final Table         table      = column.getTable();
            final String        alias      = ParseSQLUtil.getAlias(table);
            if (StrUtil.isBlank(alias)) {
                log.debug("---> [Mybatis 加密拦截器] 字段 {} 未设置归属表别名，默认使用 {} 做为字段归属表的别名...", columnName, defaultTableName);
                expression.setAlias(defaultTableName);
            } else {
                expression.setAlias(alias);
            }
            expression.setColumnName(columnName);
            expression.setIndex(i + 1);
            result.add(expression);
        }
        return result;
    }

    /**
     * 解析 update 字段的表名
     *
     * @param update 更新对象
     * @return {@link List<JSQLCryptExpression> }
     */
    private List<JSQLCryptExpression> parseUpdateColumns(Update update) {
        final String              defaultTableName = update.getTable().getName();
        final List<Column>        columns          = update.getUpdateSets().stream().flatMap(u -> u.getColumns().stream()).collect(Collectors.toList());
        List<JSQLCryptExpression> result           = new ArrayList<JSQLCryptExpression>();
        for (int i = 0; i < columns.size(); i++) {
            JSQLCryptExpression expression = new JSQLCryptExpression();
            final Column        column     = columns.get(i);
            final String        columnName = column.getColumnName();
            final Table         table      = column.getTable();
            final String        alias      = ParseSQLUtil.getAlias(table);
            if (StrUtil.isBlank(alias)) {
                log.debug("---> [Mybatis 加密拦截器] 字段 {} 未设置归属表别名，默认使用 {} 做为字段归属表的别名...", columnName, defaultTableName);
                expression.setAlias(defaultTableName);
            } else {
                expression.setAlias(alias);
            }
            expression.setColumnName(columnName);
            expression.setIndex(i + 1);
            result.add(expression);
        }
        return result;
    }

    /**
     * 创建 EncryptInfo 集合对象
     *
     * @param expression expression
     * @return {@link Collection< EncryptInfo > }
     */
    private Collection<EncryptInfo> createEncryptInfos(JSQLCryptExpression expression) {
        final Optional<EncryptInfo> encryptCondition = createEncryptInfo(expression);
        return encryptCondition.isPresent() ? Collections.singletonList(encryptCondition.get()) : Collections.emptyList();
    }

    /**
     * 创建 EncryptCondition 对象
     *
     * @param expression expression
     * @return {@link Optional< EncryptInfo > }
     */
    private Optional<EncryptInfo> createEncryptInfo(JSQLCryptExpression expression) {
        final Optional<String> tableName = findTableName(expression);
        if (tableName.isPresent()) {
            final String  columnName    = expression.getColumnName();
            final int     index         = expression.getIndex();
            final String  expressionSql = expression.getSql();
            final String  tableNameStr  = tableName.get();
            final boolean isCreate      = isContainsColumn(tableNameStr, columnName);
            if (isCreate) {
                return Optional.of(new EncryptInfo(tableNameStr, columnName, index, expressionSql));
            } else {
                return Optional.empty();
            }
        } else {
            return Optional.empty();
        }
    }

    /**
     * 是否包含列
     *
     * @param tableName  表名
     * @param columnName 列名
     * @return boolean
     */
    private boolean isContainsColumn(String tableName, String columnName) {
        return encryptRule.isContainsColumn(tableName, columnName);
    }

    /**
     * 从 tableList 数据中 获取表名
     *
     * @param columnName 列名
     * @return {@link Optional<String> }
     */
    private Optional<String> findTableNameFromMetaData(final String columnName) {
        for (JSQLTable each : tableList) {
            final String tableName = each.getTableName();
            if (isContainsColumn(tableName, columnName)) {
                return Optional.of(tableName);
            }
        }
        return Optional.empty();
    }

    /**
     * 根据表名或表别名从 tableList 中获取表名
     *
     * @param tableNameOrAlias 表名或别名
     * @return {@link String }
     */
    private String findTableNameFromSQL(final String tableNameOrAlias) {
        for (JSQLTable each : tableList) {
            final String tableName = each.getTableName();
            final String aliasName = each.getAliasName();
            if (StrUtil.equalsAnyIgnoreCase(tableNameOrAlias, tableName, aliasName)) {
                return tableName;
            }
        }
        log.error("未找到表...");
        return null;
    }

}
